[기초부터 완성까지 프론트엔드 3장]

3장

자바스크립트는 ECMAScript의 명세 구현을 목표로 개발되며, ECMaScript의 명세는 ES1을 시작으로 현재는 ES2021까지 나온 상태입니다. 3장에서는 변수를 선언하고 구문을 작성하는 규칙, 객체와 타입의 정의 등 자바스크립트의 기본개념들을 ECMAScript의 명세를 기반으로 알아보겠습니다.

변수선언

정적 타입 언어와는 다르게 자바스크립트는 느슨한 타입을 가진 언어이기 때문에 데이터 타입을 따로 명시하지 않고 변수를 선언할 수있습니다.

느슨한 타입이라고 타입이 존재하지 않는 것이 아닙니다. 변수를 선언할 때 타입을 명시하지 않는 것일 뿐, 내부적으로는 데이터의 종류에 따른 변수의 타입을 가집니다.

자바스크립트에서는 var, let, const 세 가지 키워드를 사용해 변수를 선언하며, 이 변수에 값을 할당합니다.

var

var로 선언된 변수는 기존에 선언된 변수의 값을 덮어쓰며, 함수 스코프를 기준으로 동작합니다.

var a = 1

if (isSomething) {
  var a = 2
}

console.log(a) // 2;

변수 a에 1을 할당하고 조건문 내에 변수 a를 2로 다시 선언했다. 중복된 변수명으로 선언했으나 기존 a의 변수 값을 덮어쓰며, 에러도 발생하지 않는다. var로 변수를 선언할 경우 스코프 내에 이미 동일한 식별자를 가진 변수가 존재한다면 해당 변수에 값을 재할당합니다. 위 예제 코드처럼 작성하면 버그가 발생할 때 원인을 찾기 어렵다.

블록 함수에서 선언하지 않는 변수는 모두 전역 스코프를 기준으로 선언되며, 이를 전역변수라고 부릅니다. 전역 스코프가 아닌 특정 함수내에서 var로 선언한 변수는 함수 스코프를 가집니다. 함수 스코프를 가진다는 것은 변수를 선언한 함수 내에서만 해당 변수에 접근할수 있다는 것을 의미합니다.

function foo() {
  var a = 1
  console.log(a) // 1
}

console.log(a) // error

하지만 함수 스코프는 블록을 무시해서 문제가 된다.

function foo() {
  for (var i = 0; i < 10; i += 1) {
    //
  }
  console.log(i) // 10
}
foo()

변수 i는 var를 사용했기 때문에 for문 블록 스코프를 무시한다. 그래서 함수스코프 안에있는 i를 참조가 가능하다. 이렇게 코드를 작성하면 혼란을 초래한다. 이러한 문제를 해결하기 위해 let, const가 등장합니다.

let과 const

let, constvar와 달리 재선언을 허용하지 않으며, 블록스코프를 가집니다.

let a = 1
let a = 2 // error

const b = 2
const b = 2 // error
{
  let a = 1
}

console.log(a) // error

블록 스코프를 가진다는 소리는 변수를 둘러싼 블록({}) 안에서만 해당 변수에 접근할 수 있다는 의미입니다.

function foo() {
  for (let i = 0; i < 10; i += 1) {
    console.log(i)
  }
  console.log(i) //error
}

자바스크립트를 아는 사람이라면 당연한 결과인 것 같다.

constlet과 달리 값의 변경을 허용하지 않는다. const는 변하지 않는 상수를 선언할 때 사용한다.

객체와 타입

자바스크립트 타입을 다른 언어들과 달리 매우 느슨합니다. 타입과 상관없이 car, let, const와 같은 키워드로 변수를 선언합니다. 자바스크립트 값은 원시 타입객체 타입으로 나뉩니다.

원시 타입

number, string, boolean, null, undefined, Symbol, BigInt(새로 추가됨)

원시 타입은 하나의 값만 가지며, 불변의 데이터라 연산을 해도 기존 값이 변경되지 않는다.

원시 타입을 제외한 나머지는 객체다


Symbol

심볼은 데이터의 유일함을 나타낼 때 사용하며, 생성된 심볼은 다른 어떰 심볼과도 일치하지 않습니다. 심볼은 다른 원시 타입과는 달리 Symbol() 함수를 호출해 사용합니다.

const sym1 = Symbol('key')
const sym2 = Symbol('key')

console.log(sym1 === sym2) // false

전역 심볼을 사용해서 매번 새로운 심볼을 생성하지 않고 기존심볼을 검색해 사용가능합니다.

const sym1 = Symbol.for('myApp.key')
const sym2 = Symbol.for('myApp.key')

console.log(sym1 === Symbol.for('myApp.key')) // true
console.log(sym2 === sym2) // true

심볼은 객체나 클래스에서 유일한 프로퍼티를 만들 때 사용됩니다. 심볼을 사용해 프로퍼티를 만들면 유일함이 보장되어 프로퍼티 추가시 충돌날 일이 없습니다. 또한 외부접근에서 변경을 막을수있습니다.

const user = {
  name: 'javascript',
}

const id = Symbol('id')
user[id] = 'lygggg'
console.log(user[Symbol('id')]) // undefined

예시를 하나 더 들면

const user = {
  name: 'lygggg',
}

const height = Symbol('height')
user[height] = 181

for (key in user) {
  console.log(user[key]) // lygggg
}
console.log(user) // {name: 'lygggg', Symbol(height): 181}

for문으로 출력할때는 Symbol로 정의한 데이터는 숨겨진다. 심볼로 정의한 프로퍼티를 수정할 방법이 존재하긴하는데, getOwnPropertySymbols() 메서드로 키값을 얻어서 갱신할 수 있지만 지양하는게 좋다고 한다.

객체 리터럴

const obj = {
  name: 'lygggg',
  age: 28,
}

const { name, age } = obj
console.log(name, age) // lygggg, 28

getter와 setter

getter, setter 접근자 프로퍼티를 사용해 값에 접근하거나 수정할수 있다.

const obj = {
  conut: 0,
  set count(count) {
    if (count !== null && count > 0) {
      this.count = count // 메서드 안의 this는 바로 위 객체를 가르킨다 {obj}
    }
  },
}

Object.defineProperty()와 프로퍼티 속성

정적 메서드 Object.defineProperty를 사용하면 접근자 프로퍼티를 생성할 수있다. 이 메서드는 객체에 직접 새로운 프로퍼티를 정의하거나 이미 존재하는 프로퍼티를 수정한 후 그 객체를 반환합니다.

const obj = { myName: 'javascript' }

Object.defineProperty(obj, 'name', {
  set(name) {
    if (name !== null) {
      this.myName = name
    }
  },
  get() {
    return 'java'
  },
})

console.log(obj.name) // java
console.log((obj.name = 'lygggg')) // lygggg

Object.defineProperty() 메서드는 첫 번째 인자로 대상이 되는 객체, 두 번째 인자로 추가 또는 갱신하려는 프로퍼티 명이나 심볼을 넘깁니다. 그리고 마지막 인자로 프로퍼티 서술자를 정의한 객체를 넘깁니다.

configurable : 프로퍼티의 삭제 및 서술자의 변경 가능 여부를 결정합니다.

enumerable : 열거 시 프로퍼티의 노출 여부를 결정합니다. 열거 시 프로퍼티가 노출된다면 enumerable 속성값은 true이며 디폴트값은 false입니다.

writable : 프로퍼티의 수정 가능 여부를 결정합니다. 수정이 가능하다면 writable 속성 값은 true이며 디폴트값은 false입니다.

value : 프로퍼티의 값으로 디폴트값은 undefined입니다.

get : getter 접근자 프로퍼티 메서드로 디폴트값은 undefined입니다.

set : setter 접근자 프로퍼티 메서드로 디폴트값은 undefined입니다.

배열 리터럴

자바스크립트 배열은 비규질적 배열이라고한다. 다양한 타입을 넣을수있다.

const arr = ['1', '2', true]

하지만 이런 데이터배열은 일관성이 없다. 배열을 사용할땐 통일된 타입의 데이터를 넣는게 좋다.

원소 접근과 동적인 원소 생성

자바스크립트에서 배열은 객체이기때문에 동적으로 프로퍼티를 추가할 수 있다.

const arr = []

arr[0] = 1
arr[2] = 2

console.log(arr) // [1, empty, 3]
console.log(arr.length) // 3

중간에 empty를 희소배열이라 한다. forEach, map같은 메서드들은 empty를 무시하지만 find같은 메소드들은 모두 탐색한다 특수한 경우가 아니라면 지양하는게 좋다.

주의할점

배열에 이름:값으로 데이터를 추가하면 length 프로퍼티가 갱신되지 않는다. 이런 방법은 권장하지 않습니다.

const arr = []
arr.foo = 'foo'
console.log(arr.length) // 0

length 프로퍼티 배열 조작 꿀팁

const arr = [1, 2, 3, 4]
arr.length = 2
console.log(arr) //[1, 2]

랩퍼(wrapper) 객체

const str = 'javascript'
console.log(str.length) // 10

문자열은 원시타입인데 어떻게 length 프로퍼티를 가질수있을까? 자바스크립트는 문자열의 프로퍼티에 접근할 때 내부적으로 문자열 값을 가지고 임시 객체로 변환합니다. 그리고 프로퍼티 접근이 종료되면 생성된 객체는 제거됩니다. 이런 과정을 박싱(Boxing)이라고 부릅니다. 숫자와 불리언타입 다 동일합니다. 박싱 과정에서 생성되는 임시 객체를 랩퍼 객체라고 합니다.

언박싱

박싱이 있다면 언박싱도 있다. 반대로 랩퍼객체를 원시타입으로 변환한다.

const num = new Number(11)
console.log(num.valueOf())